package com.strong.utils.security;

import cn.hutool.core.io.FileUtil;
import cn.hutool.core.lang.Assert;
import cn.hutool.core.util.*;
import cn.hutool.crypto.SecureUtil;
import cn.hutool.crypto.SmUtil;
import cn.hutool.crypto.asymmetric.KeyType;
import cn.hutool.crypto.asymmetric.SM2;
import com.strong.utils.JSON;
import com.strong.utils.StrongUtils;
import com.strong.utils.mvc.pojo.view.ReplyVO;
import jakarta.servlet.http.HttpServletResponse;
import lombok.extern.slf4j.Slf4j;
import org.springframework.http.MediaType;
import org.springframework.security.authentication.UsernamePasswordAuthenticationToken;

import java.io.IOException;
import java.io.PrintWriter;
import java.security.KeyPair;
import java.util.Map;
import java.util.concurrent.ConcurrentHashMap;

import static com.strong.system.SystemConstants.STR_DIRECTORY_GIT_IGNORE;

@Slf4j
public class SecurityUtils {

    /**
     * 私钥文件
     */
    static String STR_PRIVATE_KEY_FILE = StrongUtils.getStaticPath(STR_DIRECTORY_GIT_IGNORE, "private.key");

    /**
     * 公钥文件
     */
    static String STR_PUBLIC_KEY_FILE = StrongUtils.getStaticPath("src", "main", "resources", "public.key");

    /**
     * UUID文件
     */
    static String STR_UUID_FILE = StrongUtils.getStaticPath(STR_DIRECTORY_GIT_IGNORE, "uuid");

    /**
     * 私钥字节数组
     */
    public final static byte[] BYTES_PRIVATE_KEY = FileUtil.exist(STR_PRIVATE_KEY_FILE) ? FileUtil.readBytes(STR_PRIVATE_KEY_FILE) : null;

    /**
     * 公钥字节数组
     */
    public final static byte[] BYTES_PUBLIC_KEY = FileUtil.exist(STR_PUBLIC_KEY_FILE) ? FileUtil.readBytes(STR_PUBLIC_KEY_FILE) : null;

    /**
     * UUID字符串
     */
    public final static String STR_UUID = FileUtil.exist(STR_UUID_FILE) ? FileUtil.readUtf8String(STR_UUID_FILE) : null;

    /**
     * SpringSecurity错误消息名称
     */
    public static final String SPRING_SECURITY_LAST_EXCEPTION = "SPRING_SECURITY_LAST_EXCEPTION";

    /**
     * 【系统】用户名与JWT Token对应的map
     * key:   用户登录名
     * value: JWT Token
     */
    public static Map<String, String> MAP_SYSTEM_USER_TOKEN = new ConcurrentHashMap<>(8);

    /**
     * 【系统】用户名与 UsernamePasswordAuthenticationToken 对应的map
     * key:   用户登录名
     * value: UsernamePasswordAuthenticationToken
     */
    public static Map<String, UsernamePasswordAuthenticationToken> MAP_SYSTEM_USER_AUTHENTICATION = new ConcurrentHashMap<>(8);

    /**
     * 身份验证的前缀
     */
    public static final String STR_AUTHENTICATION_PREFIX = "Bearer ";

    /**
     * 加盐模板字符串
     */
    public static final String STR_SALT_TEMPLATE = "%s|||%s";

    /**
     * SM2对象
     */
    public static final SM2 SM2_OBJ = ArrayUtil.isAllNotEmpty(BYTES_PRIVATE_KEY, BYTES_PUBLIC_KEY) ?
            SmUtil.sm2(FileUtil.readBytes(STR_PRIVATE_KEY_FILE), FileUtil.readBytes(STR_PUBLIC_KEY_FILE)) : null;

    /**
     * 返回应答响应 HttpServletResponse
     *
     * @param response 响应
     * @param intCode  int代码
     * @param replyVO  回答签证官
     * @throws IOException ioexception
     */
    public static void returnReplyJsonResponse(HttpServletResponse response, int intCode, ReplyVO<?> replyVO) throws IOException {
        response.setStatus(intCode);
        response.setCharacterEncoding(CharsetUtil.UTF_8);
        response.setContentType(MediaType.APPLICATION_JSON_VALUE);
        PrintWriter printWriter = response.getWriter();
        printWriter.print(JSON.toJSONString(replyVO));
        printWriter.flush();
        printWriter.close();
    }

    /**
     * 对字符串进行签名，使用默认UUID
     *
     * @param strBeSigned 待签名字符串
     * @param strSalt     加密盐
     * @return {@link String}
     */
    public static String signBySalt(String strBeSigned, String strSalt) {
        Assert.notBlank(strBeSigned, "待签名字符串为空");
        Assert.notBlank(strSalt, "签名加密盐为空");

        return HexUtil.encodeHexStr(SM2_OBJ.sign(StrUtil.utf8Bytes(strBeSigned), StrUtil.utf8Bytes(strSalt)));
    }

    /**
     * 对字符串进行签名，使用默认UUID
     *
     * @param strBeSigned 待签名字符串
     * @return {@link String}
     */
    public static String signByUUID(String strBeSigned) {
        Assert.notBlank(strBeSigned, "待签名字符串为空");
        return HexUtil.encodeHexStr(SM2_OBJ.sign(StrUtil.utf8Bytes(strBeSigned), StrUtil.utf8Bytes(SecurityUtils.STR_UUID)));
    }

    /**
     * 对字符串签名进行校验，使用默认UUID
     *
     * @param strBeVerified 待校验字符串
     * @param strSign       签名字符串
     * @param strSalt       加密盐
     * @return boolean
     */
    public static boolean verifyBySalt(String strBeVerified, String strSign, String strSalt) {
        Assert.notBlank(strBeVerified, "待校验字符串为空");
        Assert.notBlank(strSign, "签名字符串为空");
        Assert.notBlank(strSalt, "加密盐为空");

        return SM2_OBJ.verify(StrUtil.utf8Bytes(strBeVerified), HexUtil.decodeHex(strSign), StrUtil.utf8Bytes(strSalt));
    }

    /**
     * 对字符串签名进行校验，使用默认UUID
     *
     * @param strBeVerified 待校验字符串
     * @param strSign       签名字符串
     * @return boolean
     */
    public static boolean verifyByUUID(String strBeVerified, String strSign) {
        Assert.notBlank(strBeVerified, "待校验字符串为空");
        Assert.notBlank(strSign, "签名字符串为空");
        return SM2_OBJ.verify(StrUtil.utf8Bytes(strBeVerified), HexUtil.decodeHex(strSign), StrUtil.utf8Bytes(SecurityUtils.STR_UUID));
    }

    /**
     * 生成测试加密、签名字符串
     */
    public static void generatePasswordEncode(String strPassword) {
        try {
            // 生成签名字符串
            String strPasswordSign = HexUtil.encodeHexStr(SM2_OBJ.sign(StrUtil.utf8Bytes(strPassword), StrUtil.utf8Bytes(STR_UUID)));
            log.info("  {} 签名结果 -> {}", strPassword, strPasswordSign);
            boolean boolVerify = SM2_OBJ.verify(StrUtil.utf8Bytes(strPassword), HexUtil.decodeHex(strPasswordSign), StrUtil.utf8Bytes(STR_UUID));
            Assert.isTrue(boolVerify, "签名校验错误");
            log.info("  {} 签名结果校验正确 -> {}", strPassword, strPasswordSign);
            // 生成加密的字符串
            String strEncrypt = SM2_OBJ.encryptBase64(strPassword, KeyType.PublicKey);
            log.info("  {} 加密结果 -> {}", strPassword, strEncrypt);
            // 生成解密后的字符串
            String strDecrypt = SM2_OBJ.decryptStr(strEncrypt, KeyType.PrivateKey);
            log.info("  {} 解密结果 -> {}", strEncrypt, strDecrypt);
        } catch (Exception e) {
            log.info("密钥对文件存在测试 - 失败，请尝试重新生成密钥对文件");
            System.exit(0);
        }
    }

    /**
     * 测试加解密、签名字符串
     */
    public static void testPasswordEncode() {
        // 判断密钥对文件是否存在
        if (!ArrayUtil.isAllNotEmpty(SecurityUtils.BYTES_PRIVATE_KEY, SecurityUtils.BYTES_PUBLIC_KEY)) {
            log.info("密钥对文件存在检测 - 失败");
            log.info("请使用com.strong.utils.security.SecurityUtils中的generateKeyPair重新生成密钥对");
            System.exit(0);
        }
        log.info("密钥对文件存在测试 - 通过");

        // 测试加解密、签名字符串
        SecurityUtils.generatePasswordEncode(IdUtil.simpleUUID());
        log.info("密钥对加解密测试 - 通过");
    }

    /**
     * 生成密钥对
     */
    private static void generateKeyPair() {
        // 获取密钥对象
        KeyPair pair = SecureUtil.generateKeyPair("SM2");
        // 删除现有密钥文件
        FileUtil.del(STR_PRIVATE_KEY_FILE);
        FileUtil.del(STR_PUBLIC_KEY_FILE);
        FileUtil.del(STR_UUID_FILE);
        // 写入密钥文件
        FileUtil.writeBytes(pair.getPrivate().getEncoded(), STR_PRIVATE_KEY_FILE);
        FileUtil.writeBytes(pair.getPublic().getEncoded(), STR_PUBLIC_KEY_FILE);
        FileUtil.writeUtf8String(IdUtil.simpleUUID(), STR_UUID_FILE);

        log.info("密钥对生成完毕：\n公钥：{}\n私钥：{}\n项目UUID：{}\n请注意保存好私钥文件，不要上传至服务器", STR_PRIVATE_KEY_FILE, STR_PUBLIC_KEY_FILE, STR_UUID_FILE);
    }

    /**
     * 生成加密后的数据库用户名和密码
     *
     * @param strDatabaseUsername STR数据库用户名
     * @param strDatabasePassword STR数据库密码
     */
    private static void generateDatabaseUsernameAndPassword(String strDatabaseUsername, String strDatabasePassword) {
        log.info("\n使用公钥加密数据库账号密码，需将加密字符串配置到application.yaml\n数据库用户名[{}] 加密后[{}]\n密码[{}] 加密后[{}]\n",
                strDatabaseUsername, SM2_OBJ.encryptBase64(strDatabaseUsername, KeyType.PublicKey),
                strDatabasePassword, SM2_OBJ.encryptBase64(strDatabasePassword, KeyType.PublicKey)
        );
    }

    public static void main(String[] args) {
//        generateKeyPair();
//        generatePasswordEncode("527aa76ded2e45d397aaf022a0db7ec3");
        generateDatabaseUsernameAndPassword("sa", "123456");
    }
}
